package com.lancoo.cvas.arrangecourse.coursescheduling.assignlist;

import com.lancoo.cvas.arrangecourse.common.BinaryUtil;
import com.lancoo.cvas.arrangecourse.coursescheduling.allocatetimeslotroom.AllocateTimeslotRoom;
import com.lancoo.cvas.arrangecourse.entities.ClassHour;
import com.lancoo.cvas.arrangecourse.entities.TeachingClassUnit;
import com.lancoo.cvas.arrangecourse.entities.TimeslotRoom;
import com.lancoo.cvas.arrangecourse.rulecheck.RuleService;
import com.lancoo.cvas.pojo.dto.ArrangeTeachingClassRuleDto;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * @Description 分配教室课时分组信息，父类
 * 本模块只处理，指定了对应的教室的，教室课时分组。通常如果只有一个教室，那就是针对这个教室的所有课时做分配
 * @Author Fortysun
 * @Date 2023/6/7 19:26
 **/
@Slf4j
public class SpecifiedTimelotRoom {

    //排课规则判断处理类
    private RuleService ruleService;
    //分配教室课时的处理类
    private AllocateTimeslotRoom allocateTimeslotRoom;

    /**
     * 接收外面传入的排课规则，以及教室课时分组的分配规则；
     * @param ruleService 排课规则检测类
     * @param allocateTimeslotRoom 教室课时分组分配处理；
     */
    public SpecifiedTimelotRoom(RuleService ruleService,
                                AllocateTimeslotRoom allocateTimeslotRoom) {
        this.ruleService = ruleService;
        this.allocateTimeslotRoom = allocateTimeslotRoom;
    }

    /**
     * 支持设置规则的检测
     * @param ruleService 规则的检测服务，检测规则是否符合要求
     */
    public void setRuleService(RuleService ruleService) {
        this.ruleService = ruleService;
    }

    /**
     * 开始进行分配
     * @param teachingClassUnit 需要拆班的教学班
     * @param roomList 可以用来分配给教学班的，教室场地信息，通过教学班指定的教室，进行了过滤
     * @param classHourMap 当前已经排好的课表信息，用来进行冲突检测
     */
    public TeachingClassUnit startAllocation(TeachingClassUnit teachingClassUnit,
                                              LinkedList<TimeslotRoom> roomList, //一个教室
                                             ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        if (roomList != null && !roomList.isEmpty()) {
            //取这个教室的容量，与教学班的人数进行比较，能坐下才进行分配
            if (roomList.getFirst().getCapacity() >= teachingClassUnit.getTeachingClassSize()) {
                //获取课程的连上节次信息
                Integer connectSection = teachingClassUnit.getConnectSection();
                //获取课程的上课周次信息
                String teachingClassWeeks = teachingClassUnit.getClassWeeks();


                log.info("#NormalAssign.arranging---startAllocation #--排课单元：{}",
                        teachingClassUnit.getTeachingClassName() + "--" + teachingClassUnit.getCourseName());

                //如果连上节次是1，不需要去寻找连续的几个课时
                if (connectSection.equals(1)) {
                    //找出可以分配的，符合要求的map
                    teachingClassUnit = findAvailable(teachingClassWeeks, roomList,
                            teachingClassUnit, classHourMap);
                } else {//如果连上节次大于1
                    //找出符合连上节次要求的教室课时map数据信息
                    //比如连上2节，那我们筛选出符合条件的map，只包含0101和0102，两种key对应的map
                    teachingClassUnit = findByDayList(connectSection, teachingClassUnit,
                            roomList, classHourMap);
                }
            }
        }

        return teachingClassUnit;
    }

    /**
     * 处理连上节次是1的情况
     * @param teachingClassWeeks 教学班的排课周次信息
     * @param allocationList 教学班指定教室，对应的教室课时分组信息
     * @param teachingClassUnit 待排教学班排课单元
     * @param classHourMap 已排课表信息
     */
    private TeachingClassUnit findAvailable(String teachingClassWeeks,
                                            LinkedList<TimeslotRoom> allocationList, //一个教室
                                            TeachingClassUnit teachingClassUnit,
                                            ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        //待分配的教室课时分组对象，需要转换成列表进行处理
        LinkedList<TimeslotRoom> allcatedList = new LinkedList();
        for (TimeslotRoom timeslotRoom : allocationList) {
            //如果每个课时编码，对应的教室课时分组信息列表的大小，等于教学班指定的教室数量
            //并且周次信息都符合分配要求
            if (isWeeksAvailable(teachingClassWeeks, timeslotRoom)) {
                //加入之前，先清理之前的数据
                allcatedList.clear();
                //将合适的对象，加入处理的列表
                allcatedList.add(timeslotRoom);

                //进行规则适配以及分配
                teachingClassUnit = ruleCheckAndAllocate(teachingClassUnit, allcatedList, classHourMap);
                //如果分配成功
                if (teachingClassUnit.getTimeslotRoomList() != null
                        && !teachingClassUnit.getTimeslotRoomList().isEmpty()) {

                    //直接跳出循环
                    break;
                }
            }
        }

        return teachingClassUnit;
    }
    /**
     * 将待分配的教室课时列表信息，按天进行分组，遍历
     * @param connectSection 连上节次
     * @param teachingClassUnit 待分配的教学班排课单元
     * @param roomList 待分配的某个教室的教室课时分组列表，
     * @param classHourMap 已生成的课表map
     * @return
     */
    private TeachingClassUnit findByDayList(Integer connectSection,
                                            TeachingClassUnit teachingClassUnit,
                                            LinkedList<TimeslotRoom> roomList, //一个教室
                                            ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        if (roomList == null || roomList.size() < connectSection) {
            return teachingClassUnit;
        }
        //按照每周的上课时间，对列表进行分类，如周一是01，周二是02，进行分类
        Map<String, LinkedList<TimeslotRoom>> dayMap = roomList.stream()
                .collect(Collectors.groupingBy(TimeslotRoom::getDayNo, Collectors.toCollection(LinkedList::new)));
        //遍历某个教室，可以分配的教室课时分组信息，例如：从周一到周五，依次遍历
        for (Map.Entry<String, LinkedList<TimeslotRoom>> entry : dayMap.entrySet()) {
            //获取某天的教室课时分组信息
            LinkedList<TimeslotRoom> dayList = entry.getValue(); //一天的课时

            teachingClassUnit = findByHalfDayList(connectSection, teachingClassUnit, dayList, classHourMap);
            //如果分配成功
            if (teachingClassUnit.getTimeslotRoomList() != null
                    && !teachingClassUnit.getTimeslotRoomList().isEmpty()) {
                //清空map
                dayMap.clear();
                //释放内存，等待GC回收
                dayMap = null;

                //直接返回
                return teachingClassUnit;
            }
        }
        //清空map
        dayMap.clear();
        //释放内存，等待GC回收
        dayMap = null;
        return teachingClassUnit;
    }

    /**
     * 将待分配的教室课时列表信息，按半进行分组，遍历
     * @param connectSection 连上节次
     * @param teachingClassUnit 待分配的教学班排课单元
     * @param dayList 待分配的某天的教室课时分组列表，
     * @param classHourMap 已生成的课表map
     * @return
     */
    private TeachingClassUnit findByHalfDayList(Integer connectSection,
                                                TeachingClassUnit teachingClassUnit,
                                                LinkedList<TimeslotRoom> dayList, //一天的课时
                                                ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        if (dayList == null || dayList.size() < connectSection) {
            return teachingClassUnit;
        }
        //按照课时分布在1上午、2下午、3晚上，进行分类
        Map<Integer, LinkedList<TimeslotRoom>> periodTypeMap = dayList.stream()
                .collect(Collectors.groupingBy(TimeslotRoom::getPeriodType, Collectors.toCollection(LinkedList::new)));
        //遍历某一天，可以分配的教室课时分组信息，例如：上午、下午、晚上，依次遍历
        for (Map.Entry<Integer, LinkedList<TimeslotRoom>> entry : periodTypeMap.entrySet()) {
            //获取某个半天天的教室课时分组信息
            LinkedList<TimeslotRoom> halfDayList = entry.getValue(); //半天的课时
            //半天内，可分配的教室课时分组信息列表的大小
            int size = halfDayList.size();
            if (size < connectSection) {
                continue;
            } else if (size == connectSection) {
                //直接拿半天内的可分配列表，进行处理，不需要再通过连上节次，去遍历查找合适的教室课时分组信息列表
                teachingClassUnit = getHalfDayContinuousList(teachingClassUnit, halfDayList, classHourMap);
            }
            else {
                //获取半天内，符合条件的，连上节次的，教室课时分组信息列表
                teachingClassUnit = findHalfDayContinuousList(connectSection, teachingClassUnit, halfDayList, classHourMap);
            }

            //如果分配成功
            if (teachingClassUnit.getTimeslotRoomList() != null
                    && !teachingClassUnit.getTimeslotRoomList().isEmpty()) {
                //清空map
                periodTypeMap.clear();
                //释放内存，等待GC回收
                periodTypeMap = null;

                //直接返回
                return teachingClassUnit;
            }
        }
        //清空map
        periodTypeMap.clear();
        //释放内存，等待GC回收
        periodTypeMap = null;
        return teachingClassUnit;
    }

    /**
     * 将待分配的教室课时列表信息，按半天进行分组，遍历
     * @param connectSection 连上节次
     * @param teachingClassUnit 待分配的教学班排课单元
     * @param halfDayList 待分配的某个半天的教室课时分组列表，
     * @param classHourMap 已生成的课表map
     * @return
     */
    private TeachingClassUnit findHalfDayContinuousList(Integer connectSection,
                                                        TeachingClassUnit teachingClassUnit,
                                                        LinkedList<TimeslotRoom> halfDayList, //半天的课时
                                                        ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {

        if (halfDayList == null || halfDayList.size() < connectSection) {
            return teachingClassUnit;
        }

        //将半天内的教室课时分组信息列表，按照课时编码从小到大进行排序，
        // 比如周一上午，第1节0101、第二节0102、第三节0103、第四节0104
        Collections.sort(halfDayList, Comparator.comparing(TimeslotRoom::getTimeslotCode));
        //某个半天的教室课时分组信息列表的长度
        int size = halfDayList.size();

        //每次循环索引跳过的值
        int jumpValue = 1;
        //如果能够整除，比如上午4节，连上节次是2，或者1
        if (size % connectSection == 0) {
            //每次跳过连上节次
            //比如：上午4节，连上2节
            //那么循环的时候，连在一起的，应该是1、2；3、4；
            //对应的索引就是0、2
            jumpValue = connectSection;
        } else {//如果不能整除，比如上午4节，连上3节；上午3节，连上2节
            //迭代的时候，只需要迭代length次即可，
            //即，如果上午4节，连上3节，只需要选择，1、2、3节和2、3、4节即可，
            // 对应的循环的索引，只需要是0和1
            size = size -connectSection + 1;
            jumpValue = 1;
        }
        //开始寻找合适的，连上的课时信息
        for (int i = 0; i < size; i = i + jumpValue) {
            //获取连上的教室课时分组信息列表
            LinkedList<TimeslotRoom> continuousList = getContinuousList(i, connectSection, halfDayList);
            //如果取的key是连续的，并且是可用的
            if (isContinuousListAvailable(continuousList, teachingClassUnit.getClassWeeks())) {
                //进行规则适配以及分配
                teachingClassUnit = ruleCheckAndAllocate(teachingClassUnit, continuousList, classHourMap);
                //如果分配成功
                if (teachingClassUnit.getTimeslotRoomList() != null
                        && !teachingClassUnit.getTimeslotRoomList().isEmpty()) {
                    //直接返回
                    return teachingClassUnit;
                }
            }
        }

        return teachingClassUnit;
    }

    /**
     * 将待分配的教室课时列表信息，按半天进行分组，遍历
     * @param teachingClassUnit 待分配的教学班排课单元
     * @param halfDayList 待分配的某个半天的教室课时分组列表，
     * @param classHourMap 已生成的课表map
     * @return
     */
    private TeachingClassUnit getHalfDayContinuousList(TeachingClassUnit teachingClassUnit,
                               LinkedList<TimeslotRoom> halfDayList, //半天的课时
                               ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {

        if (halfDayList == null || halfDayList.isEmpty()) {
            return teachingClassUnit;
        }

        //将半天内的教室课时分组信息列表，按照课时编码从小到大进行排序，
        // 比如周一上午，第1节0101、第二节0102、第三节0103、第四节0104
        Collections.sort(halfDayList, Comparator.comparing(TimeslotRoom::getTimeslotCode));

        //如果取的key是连续的，并且是可用的
        if (isContinuousListAvailable(halfDayList, teachingClassUnit.getClassWeeks())) {
            //进行规则适配以及分配
            teachingClassUnit = ruleCheckAndAllocate(teachingClassUnit, halfDayList, classHourMap);
        }

        return teachingClassUnit;
    }

    /**
     * 排课规则的适配，并且进行分配
     * @param teachingClassUnit 教学班排课单元
     * @param continuousList 待分配的教室课时分组信息列表
     * @param classHourMap 已排课表数据
     * @return
     */
    public TeachingClassUnit ruleCheckAndAllocate(TeachingClassUnit teachingClassUnit,
                                                  LinkedList<TimeslotRoom> continuousList,
                                                  ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        if (continuousList != null && !continuousList.isEmpty()) {
            log.info("#NormalAssign.arranging---ruleCheckAndAllocate #--排课单元 开始进行规则检测：{}",
                    teachingClassUnit.getTeachingClassName() + "--" + teachingClassUnit.getCourseName());

            //如果符合需求,符合优先排规则、基础规则、跨校区规则、冲突检测规则、禁排规则，无课时连续性检测
            if (this.ruleService.isSuitable(teachingClassUnit, continuousList, classHourMap)) {
                log.info("#NormalAssign.arranging---ruleCheckAndAllocate #--排课单元通过规则检测，开始排课时：{}",
                        teachingClassUnit.getTeachingClassName() + "--" + teachingClassUnit.getCourseName());
                //开始进行教室课时分组信息的分配；
                LinkedList<TimeslotRoom> allocatedList = allocateTimeslotRoom.start(teachingClassUnit, continuousList);
                //如果分配成功，说明列表不为空
                if (allocatedList != null && !allocatedList.isEmpty()) {
                    //将分配的教室课时分组信息列表，赋值给教学班排课单元对象
                    teachingClassUnit.setTimeslotRoomList(allocatedList);
                }

                return teachingClassUnit;
            }
        }
        log.info("#NormalAssign.arranging---ruleCheckAndAllocate #--排课单元 规则检测不通过：{}",
                teachingClassUnit.getTeachingClassName() + "--" + teachingClassUnit.getCourseName());
        return teachingClassUnit;
    }

    /**
     * 按照连上节次，获取指定数量的教室课时分组信息
     * @param index 获取的起始位置
     * @param connectSection 连上节次，即获取的数量
     * @param periodTypeList 源头列表信息，即教室课时分组信息
     * @return
     */
    private LinkedList<TimeslotRoom> getContinuousList(int index,
                                                       Integer connectSection,
                                                       LinkedList<TimeslotRoom> periodTypeList) {
        //定义返回的列表信息
        LinkedList<TimeslotRoom> retList = null;

        if (periodTypeList != null && !periodTypeList.isEmpty()) {
            //初始化列表
            retList = new LinkedList<>();
            //按照连上节次，取连续的几个教室课时分组对象
            for (int i = 0; i < connectSection; i++) {
                retList.add(periodTypeList.get(index + i));
            }
        }

        return retList;
    }

    /**
     * 判断是否连续，并且可用
     * @param continuousList 待分配的，教室课时分组信息，对应的课时编码列表
     * @param teachingClassWeeks 教学班的排课周次信息
     * @return
     */
    private Boolean isContinuousListAvailable(List<TimeslotRoom> continuousList, //半天的课时
                                              String teachingClassWeeks) {
        //如果列表为空，返回false
        if (continuousList == null || continuousList.isEmpty()) {
            return false;
        }
        //将列表值按照从小到大排序
        Collections.sort(continuousList, Comparator.comparing(TimeslotRoom::getTimeslotCode));

        //遍历找到的课时，如果是101，102，那么代表列表是连续的
        for (int i = 0 ; i < continuousList.size(); i++) {
            //获取当前的教室课时分组对象
            TimeslotRoom curTimeslotRoom = continuousList.get(i);
            //如果周次是可分配的
            if (isWeeksAvailable(teachingClassWeeks, curTimeslotRoom)) {
                //从第2个开始
                if (i > 0) {
                    //获取上一个教室课时分组对象
                    TimeslotRoom preTimeslotRoom = continuousList.get(i - 1);
                    //获取当前教室课时分组对象的课时编码
                    String curTimeslotCode = curTimeslotRoom.getTimeslotCode();
                    //获取上一个教室课时分组对象的课时编码
                    String preTimeslotCode = preTimeslotRoom.getTimeslotCode();

                    //如果两两相减的值，不是1，说明是不连续
                    if (!isTimeslotCodeContinuous(curTimeslotCode, preTimeslotCode)) {
                        //不连续，返回false
                        return false;
                    }
                }
            } else {
                //只要有一个的周次不能用，就直接return false；
                //必须所有的周次都能分配才行~
                return false;
            }

        }
        //说明连续
        return true;
    }

    /**
     * 比较两个课时编码，是否相减的值是1
     * @param curTimeslotCode 当前的课时编码
     * @param preTimeslotCode 上一个课时编码
     * @return
     */
    private Boolean isTimeslotCodeContinuous(String curTimeslotCode, String preTimeslotCode) {
        try {
            //将其转换成整型
            Integer curCode = Integer.parseInt(curTimeslotCode);
            Integer preCode = Integer.parseInt(preTimeslotCode);

            return (curCode - preCode == 1);
        } catch (Exception ex) {
            log.error("SpecifiedTimelotRoom--isTimeslotCodeContinuous:TimeslotCode转换成整型失败~！！");
            return false;
        }
    }

    /**
     * 判断当前待分配的教室课时map中，对应的list元素里面的周次，
     * 是否能够安排下教学班排课单元的周次信息
     * @param teachingClassWeeks 教学班的周次信息
     * @param timeslotRoom 待分配的教室课时分组对象
     * @return
     */
    private Boolean isWeeksAvailable(String teachingClassWeeks,
                                     TimeslotRoom timeslotRoom) {
        if (timeslotRoom != null) {

            String timeslotRoomWeeks = timeslotRoom.getWeeks();
//            //将两个二进制字符串，进行与操作,比如10101和11111，进行与操作，结果是10101
//            String result = BinaryUtil.binaryAnd(timeslotRoomWeeks, teachingClassWeeks);
//            //如果周次信息不能分配，直接返回false
//            if (!teachingClassWeeks.equals(result)) {
//                return false;
//            }
            //如果周次信息不能分配，直接返回false
            if (!BinaryUtil.binaryAndResultEqual(timeslotRoomWeeks, teachingClassWeeks)) {
                return false;
            }
            //否则说明全部都能分配，返回true
            return true;
        }
        //不能分配，返回false
        return false;
    }
}